package com.lzh.chatnote.webSocket.service.impl;

import cn.dev33.satoken.context.mock.SaTokenContextMockUtil;
import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.stp.SaTokenInfo;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.stp.parameter.SaLoginParameter;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.lzh.chatnote.chat.domain.dto.message.PushMsgDTO;
import com.lzh.chatnote.common.config.threadPoolConfig.ThreadPoolConfig;
import com.lzh.chatnote.common.domain.dto.ScanLoginDTO;
import com.lzh.chatnote.common.utils.RedisUtils;
import com.lzh.chatnote.user.config.loginConfig.GiteeConfigProperties;
import com.lzh.chatnote.user.config.loginConfig.GithubConfigProperties;
import com.lzh.chatnote.user.config.loginConfig.QqConfigProperties;
import com.lzh.chatnote.user.dao.UserDao;
import com.lzh.chatnote.user.domain.entity.User;
import com.lzh.chatnote.user.mapper.UserMapper;
import com.lzh.chatnote.user.service.cache.UserOnlineCache;
import com.lzh.chatnote.user.service.event.UserOnlineEvent;
import com.lzh.chatnote.webSocket.NettyUtil;
import com.lzh.chatnote.webSocket.domain.vo.dto.WSChannelExtraDTO;
import com.lzh.chatnote.webSocket.domain.vo.req.ws.WSReqUrlType;
import com.lzh.chatnote.webSocket.domain.vo.resp.WSBaseResp;
import com.lzh.chatnote.webSocket.domain.vo.resp.ws.WSLoginSuccess;
import com.lzh.chatnote.webSocket.enums.LoginTypeEnum;
import com.lzh.chatnote.webSocket.service.WebSocketService;
import com.lzh.chatnote.webSocket.service.adapter.WSAdapter;
import io.netty.channel.Channel;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.bean.result.WxMpQrCodeTicket;
import me.zhyd.oauth.config.AuthConfig;
import me.zhyd.oauth.request.AuthGiteeRequest;
import me.zhyd.oauth.request.AuthGithubRequest;
import me.zhyd.oauth.request.AuthQqRequest;
import me.zhyd.oauth.request.AuthRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReentrantLock;

@Slf4j
@Component
public class WebSocketServiceImpl implements WebSocketService {

    private static final Duration EXPIRE_TIME = Duration.ofHours(1);

    /**
     * 本地缓存，缓存code - Channel的关系
     */
    private static final Cache<Integer, Channel> WAIT_LOGIN_CACHE = Caffeine.
            newBuilder()
            .expireAfterWrite(EXPIRE_TIME)
            .maximumSize(100)
            .build();

    private static final String LOGIN_CODE = "loginCode";

    /**
     * 所有已连接的websocket连接列表和一些额外参数（记录channel和参数的映射容器）
     */
    private static final ConcurrentHashMap<Channel, WSChannelExtraDTO> ONLINE_WS_MAP = new ConcurrentHashMap<>();

    /**
     * 所有在线的用户和对应的socket（用户ID和Channel的映射关系））
     */
    private static final ConcurrentHashMap<Long, CopyOnWriteArrayList<Channel>> ONLINE_UID_MAP = new ConcurrentHashMap<>();

    @Resource
    private UserMapper userMapper;
    @Resource
    private GiteeConfigProperties giteeConfigProperties;
    @Resource
    private GithubConfigProperties githubConfigProperties;
    @Resource
    private QqConfigProperties qqConfigProperties;
    //公众号登录
    @Resource
    private WxMpService wxMpService;
    @Resource
    private UserDao userDao;
    @Resource
    private UserOnlineCache userOnlineCache;

//    @Resource
//    private JwtUtils jwtUtils;
//
//    //第三方登录服务qq ，gitee， github
//    @Resource
//    private AuthLoginService authLoginService;
//
//    @Resource
//    private UserCache userCache;

    @Autowired
    private ApplicationEventPublisher applicationEventPublisher;

    @Autowired
    @Qualifier(ThreadPoolConfig.WS_EXECUTOR)
    private ThreadPoolTaskExecutor threadPoolTaskExecutor;


    /**
     * 推送给所有在线用户
     *
     * @param pushMsgDTO 推送消息
     */
    public void pushOnlineAllUser(PushMsgDTO pushMsgDTO) {
        ONLINE_UID_MAP.forEach((uid, channels) -> {
            //线程池执行发送消息
            threadPoolTaskExecutor.execute(() -> sendMsgChannel(channels, pushMsgDTO.getWsBaseResp()));
        });
    }

    /**
     * 给指定用户发送消息
     * @param pushMsgDTO 推送消息
     */
    public void pushListUser(PushMsgDTO pushMsgDTO) {
        List<Long> pushUids = pushMsgDTO.getPushUids();
        pushUids.forEach(uid -> {
            CopyOnWriteArrayList<Channel> channels = ONLINE_UID_MAP.get(uid);
            if (CollUtil.isNotEmpty( channels)) {
                //线程池执行发送消息
                threadPoolTaskExecutor.execute(() -> sendMsgChannel(channels, pushMsgDTO.getWsBaseResp()));
            }
        });
    }


    public void sendMsgChannel(CopyOnWriteArrayList<Channel>  channels, WSBaseResp<?> wsBaseResp) {
        channels.forEach(channel -> {
            if(channel.isActive()){
                channel.writeAndFlush(new TextWebSocketFrame(JSONUtil.toJsonStr(wsBaseResp)));
            }
        });
    }


    private Integer generateCode(Channel channel) {
        ReentrantLock reentrantLock = new ReentrantLock();
        reentrantLock.lock();
        try {
            Integer code;
            do {
                code = RedisUtils.integerInc(LOGIN_CODE, 1, TimeUnit.HOURS);
            } while (WAIT_LOGIN_CACHE.asMap().containsKey(code));
            WAIT_LOGIN_CACHE.put(code, channel);
            return code;
        } finally {
            reentrantLock.unlock();
        }
    }

    @SneakyThrows
    @Override
    public void handleLoginReq(Channel channel, WSReqUrlType data) {
        //    生成验证码
        Integer code = generateCode(channel);
        String url;
        if (LoginTypeEnum.WEIXIN.type.equals(data.getLoginType())) {
            //生成二维码
            //第三方回调取出对应的code是否是网站生成的验证码
            WxMpQrCodeTicket wxMpQrCodeTicket = wxMpService.getQrcodeService().qrCodeCreateTmpTicket(code, 600);
            url = wxMpQrCodeTicket.getUrl();
        } else {
            //生成第三方授权连接地址
            AuthRequest authRequest = getAuthRequest(data.getLoginType());
            //类似于微信的openid用于第三方回调时候取出对应的code
            //url = authRequest.authorize(AuthStateUtils.createState());
            url = authRequest.authorize(code.toString());
        }

        //同一封装返回数据格式
        //使用websocket回应前端
        channel.writeAndFlush(new TextWebSocketFrame(JSONUtil.toJsonStr(WSAdapter.buildLoginResp(url))));
    }

    @Override
    public Boolean loginSuccess(Integer loginCode, Long uuid) {
        Channel channel = WAIT_LOGIN_CACHE.getIfPresent(loginCode);
        if (Objects.isNull(channel)) {
            return Boolean.FALSE;
        }
        //获取用户信息
        User user = userMapper.selectById(uuid);
        //移除验证码
        WAIT_LOGIN_CACHE.invalidate(loginCode);
        //获取当前用户已经登录的token，或者生成当前登录的token
        String token = getCurrentToken(uuid);
        //用户登录进行缓存token
        //WAIT_LOGIN_CACHE.invalidate(loginCode);
        //用户登录 做下面几件事情
        //1更新上线列表
        //2返回给用户登录成功
        //3发送给对应的用户让好友知道上线成功
        //4发送用户上线事件
        loginSuccessHandle(channel, user, token);
        return Boolean.TRUE;
    }

    private String getCurrentToken(Long id) {
        String token = StpUtil.getTokenValueByLoginId(id);
        if (StrUtil.isBlank(token)) {
            // 初始化 Sa-Token 上下文
            // 初始化 Sa-Token 上下文// ✅ 初始化 Sa-Token 上下文
//            SaTokenContext context = new SaTokenThreadLocalContextHook();
//            SaTokenContext.setByThreadLocal(context);

            // ✅ 初始化 Sa-Token 上下文
//            SaTokenContext context = new SaTokenContextHook();
//            SaTokenContext.setByThreadLocal(context);
            //这里使用的是sa-token的jwt的简单模式，导致token每次获取的是不一样的，setExtra会失效

            AtomicReference<SaTokenInfo> tokenInfo = new AtomicReference<>();
            SaTokenContextMockUtil.setMockContext(() -> {
                StpUtil.login(id, new SaLoginParameter().setExtra("id", id));
                // 第2步，获取 Token  相关参数
                tokenInfo.set(StpUtil.getTokenInfo());
            });
            return tokenInfo.get().getTokenValue();
        }
        return token;
    }

    @Override
    public void testApplicationEventPublisher() {
//        applicationEventPublisher.publishEvent(new UserOnlineEvent(this, new User()));
    }

    @Override
    public void connect(Channel channel) {
        ONLINE_WS_MAP.put(channel, new WSChannelExtraDTO());
    }

    @Override
    public void authorize(Channel channel, String token) {
        try {
            Object loginIdByToken = StpUtil.getLoginIdByToken(token);
            if (Objects.nonNull(loginIdByToken)) {
                //旧的token依然有效，尝试继续登录
                Long uid = Long.valueOf(loginIdByToken.toString());
                User user = userDao.getById(uid);
                loginSuccessHandle(channel, user, token);
            } else {
                //用户未登录，token解析失败
                channel.writeAndFlush(new TextWebSocketFrame(JSONUtil.toJsonStr(WSAdapter.buildInvalidateTokenResp())));
            }
        } catch (NotLoginException e) {
            //用户未登录，token解析失败
            channel.writeAndFlush(new TextWebSocketFrame(JSONUtil.toJsonStr(WSAdapter.buildInvalidateTokenResp())));
        }
    }

    /**
     * 用户确认登录处理
     *
     * @param scanLoginDTO 扫码验证吗
     */
    @Override
    public void sureLoginHandle(ScanLoginDTO scanLoginDTO) {
        Channel channel = WAIT_LOGIN_CACHE.getIfPresent(scanLoginDTO.getCode());
        if (Objects.isNull(channel)) {
            log.info("用户确认登录当前用户验证码过期{}", scanLoginDTO.getCode());
            return;
        }
        channel.writeAndFlush(new TextWebSocketFrame(JSONUtil.toJsonStr(WSAdapter.buildSureLoginResp())));
    }

    private void loginSuccessHandle(Channel channel, User user, String token) {
        //1更新上线列表
        online(channel, user.getId());
        //2返回给用户登录成功
        boolean hasPower = true;
        //3.相应给客户端登录成功并发送当前登录的用户信息
        sendMsg(channel, WSAdapter.buildLoginSuccessMessageResp(user, token, hasPower));
//        //4发送登录信息
        boolean online = userOnlineCache.isOnline(user.getId());
        if (!online) {
            //    发送登录事件
            user.setLastOptTime(LocalDateTime.now());
            //设置当前用户的ip
            user.refreshIp(NettyUtil.getAttr(channel, NettyUtil.IP));
            //用户上线事件
            applicationEventPublisher.publishEvent(new UserOnlineEvent(this, user));
        }
    }


    private void sendMsg(Channel channel, WSBaseResp<WSLoginSuccess> wsLoginSuccessWSBaseResp) {
        channel.writeAndFlush(new TextWebSocketFrame(JSONUtil.toJsonStr(wsLoginSuccessWSBaseResp)));
    }


    private void online(Channel channel, Long uuid) {

        //初始化ChannelExtraDTO扩展信息
        getOrInitChannelExt(channel).setUid(uuid);

        //初始化在线列表
        ONLINE_UID_MAP.computeIfAbsent(uuid, uui -> new CopyOnWriteArrayList<>());

        //把当前登录的用户放入在线列表中
        ONLINE_UID_MAP
                .get(uuid)
                .add(channel);

        //把当前登录用户ID放入Channel通道中（Channel信道很像以前的session对象可以存放数据）
        NettyUtil.setAttr(channel, NettyUtil.UID, uuid);
    }


    private WSChannelExtraDTO getOrInitChannelExt(Channel channel) {
        //获取channel隐射的额外参数
        WSChannelExtraDTO newWSChannelExtra = ONLINE_WS_MAP.getOrDefault(channel, new WSChannelExtraDTO());

        //获取老的额外参数，并且把新的放入其中
        WSChannelExtraDTO oldWSChannelExtra = ONLINE_WS_MAP.putIfAbsent(channel, newWSChannelExtra);

        return Objects.isNull(oldWSChannelExtra) ? newWSChannelExtra : oldWSChannelExtra;
    }


    private AuthRequest getAuthRequest(String source) {
        AuthRequest authRequest = null;
        switch (source) {
            case "gitee":
                authRequest = new AuthGiteeRequest(AuthConfig
                        .builder()
                        .clientId(giteeConfigProperties.getAppId())
                        .clientSecret(giteeConfigProperties.getAppSecret())
                        .redirectUri(giteeConfigProperties.getRedirectUrl())
                        .build()
                );
                break;
            case "github":
                authRequest = new AuthGithubRequest(AuthConfig
                        .builder()
                        .clientId(githubConfigProperties.getAppId())
                        .clientSecret(githubConfigProperties.getAppSecret())
                        .redirectUri(githubConfigProperties.getRedirectUrl())
                        .build()
                );
                break;
            case "qq":
                authRequest = new AuthQqRequest(AuthConfig
                        .builder()
                        .clientId(qqConfigProperties.getAppId())
                        .clientSecret(qqConfigProperties.getAppSecret())
                        .redirectUri(qqConfigProperties.getRedirectUrl())
                        .build()
                );
                break;
            default:
                break;
        }
        return authRequest;
    }
}
